/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package edu.isi.wings.common.logging;

/*
 * The following is the original copy right from gov.lbl.netlogger.LogMessage
 * Copyright (c) 2004, The Regents of the University of California, through 
 * Lawrence Berkeley National Laboratory (subject to receipt of any required 
 * approvals from the U.S. Dept. of Energy).  All rights reserved.
 */

import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.locks.*;

/**
 * This is a modification of gov.lbl.netlogger.LogMessage
 * 
 * This class lets you easily construct a set of typed (name, value) pairs that
 * formats itself as a CEDPS Best Practices log message.
 * <p>
 * Name and value pairs are added into the record with add() methods which, by
 * virtue of returning the newly modified LogMessage instance, can be chained
 * together.
 * <p>
 * The user can set the timestamp to something other than the time of the call
 * by calling setTimeStamp{Millis,Nanos}() as part of the chain.
 * </p>
 * <p>
 * To format the message, call toString(). The output format is <a
 * href="http://www.cedps.net/wiki/index.php/LoggingBestPractices">CEDPS
 * "Best Practices" format</a>.
 * </p>
 * <p>
 * Since the addition of the nanosecond timestamp (which is rounded down to
 * microseconds, and no I don't want to discuss it), this class requires Java
 * 1.5
 * </p>
 * 
 * @author Dan Gunter dkgunter@lbl.gov
 * @author Wolfgang Hoschek whoschek@lbl.gov
 * @author Paul Groth pgroth@isi.edu
 */
public class EventLogMessage {

	// Variables
	private final StringBuffer buf = new StringBuffer(256); // set initial
	
	private static final boolean DISABLED = true;
	
	// capacity for
	// efficiency/memory
	// trade-off

	private static long micro0, nano0, micro1;
	private long micro2;

	// Static Variables
	private static String timeString = null;
	private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
	private static final GregorianCalendar calendar = new GregorianCalendar(
			TimeZone.getTimeZone("UTC"));
	private static Lock timeStringLock = new ReentrantLock();

	// Constants
	public static final String APPENDER = "KEYVALUE";

	public static final String EVENT_KW = "event";
	public static final String DATE_KW = "ts";
	public static final String FAKE_DATE = "1999-01-01T11:59:59.999999Z";
	private final int dateStart = DATE_KW.length() + 1;
	private final int dateEnd = dateStart + FAKE_DATE.length() - 8;
	private final int usecStart = dateEnd + 1;
	@SuppressWarnings("unused")
	private final int usecEnd = dateEnd + 7;
	public static final String LEVEL_KW = "level=";

	private static final char[] DIGIT = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

	private static Escape escape = new Escape();

	/**
	 * Create a new LogMessage at the current time with a given event name.
	 * 
	 * The timestamp is set at creation time, but can be changed later with
	 * <code>setTimeStampMillis</code> or <code>setTimeStampNanos</code>.
	 * 
	 * @param eventName
	 *            Name of this logging event.
	 * @see #setTimeStampMillis
	 * @see #setTimeStampNanos
	 */
	protected EventLogMessage(String eventName) {
		add(DATE_KW, FAKE_DATE);
		add(EVENT_KW, eventName);
		long nano1 = System.nanoTime();
		/* calculate timestamp in microseconds */
		micro2 = (nano1 - nano0) / 1000 + micro0;
	}

	/**
	 * Add a string.
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage add(String key, String value) {
	  if(DISABLED) 
	    return this;
		buf.append(key);
		buf.append("=");
		buf.append(value);
		buf.append(" ");
		return this;
	}

	/**
	 * Add a string. Strings automatically have quotes around them and are
	 * escaped
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage addWQ(String key, String value) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append("\"");
		buf.append(escape.escape(value));
		buf.append("\"");
		buf.append(" ");
		return this;

	}

	/**
	 * Add an int.
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage add(String key, int value) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append(value);
		buf.append(" ");
		return this;
	}

	/**
	 * Add a long.
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage add(String key, long value) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append(value);
		buf.append(" ");
		return this;
	}

	/**
	 * Add a float.
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage add(String key, float value) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append(value);
		buf.append(" ");
		return this;
	}

	/**
	 * Add a double.
	 * 
	 * @return Self-reference, so calls can be chained
	 */
	public EventLogMessage add(String key, double value) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append(value);
		buf.append(" ");
		return this;
	}

	/**
	 * Add a key,value pair The result looks like: key = (pairKey, pairValue)
	 * 
	 * @param key
	 * @param pairKey
	 * @param pairValue
	 */
	public EventLogMessage addPair(String key, String pairKey, String pairValue) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append("(");
		buf.append(pairKey);
		buf.append(",");
		buf.append(pairValue);
		buf.append(")");
		buf.append(" ");
		return this;
	}

	public EventLogMessage addTime(String key, long timeInMillis) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append(format.format(new Date(timeInMillis)));
		buf.append(" ");
		return this;
	}

	/**
	 * Add a Map to the buffer. Represents maps as a series of (key, value) in
	 * quotes. The method assumes that both keys and values toString method
	 * returns a "nice" string representation. The method escapes all quotes
	 * 
	 * @param key
	 * @param map
	 * @return
	 */
	public EventLogMessage addMap(String key, Map<?, ?> map) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append("\"");
		StringBuffer ps = new StringBuffer();
		for (Object x : map.keySet()) {
			ps.append("(");
			ps.append(x.toString());
			if (map.get(x) != null) {
				ps.append(",");
				ps.append(map.get(x).toString());
			}
			ps.append(")");
		}
		buf.append(escape.escape(ps.toString()));
		buf.append("\"");
		buf.append(" ");
		return this;
	}

	public EventLogMessage addList(String key, Collection<?> list) {
	   if(DISABLED) 
	      return this;
		buf.append(key);
		buf.append("=");
		buf.append("\"");
		int i = 0;
		for (Object item : list) {
			if (i > 0)
				buf.append(",");
			buf.append(escape.escape(item.toString()));
			i++;
		}
		buf.append("\"");
		buf.append(" ");
		return this;
	}

	/**
	 * Add an natural language message to the log message. The string is put in
	 * quotes and is escaped
	 * 
	 * @param msg
	 * @return
	 */
	public EventLogMessage addMsg(String msg) {
	   if(DISABLED) 
	      return this;
		buf.append(LoggingKeys.MSG);
		buf.append("=");
		buf.append("\"");
		buf.append(escape.escape(msg));
		buf.append("\"");
		buf.append(" ");
		return this;
	}

	/**
	 * Set the timestamp from milliseconds returned by
	 * System.currentTimeMillis().
	 * 
	 * @return 'this' so we can chain
	 */
	public EventLogMessage setTimeStampMillis(long millis) {
		micro2 = millis * 1000;
		return this;
	}

	/**
	 * Set the timestamp from nanoseconds returned by System.nanoTime().
	 * 
	 * @return 'this' so we can chain
	 */
	public EventLogMessage setTimeStampNanos(long nano1) {
		micro2 = (nano1 - nano0) / 1000 + micro0;
		return this;
	}

	/**
	 * Format a message in CEDPS Best Practices format.
	 * 
	 * @return Formatted message string
	 * @see <a
	 *      href="http://www.cedps.net/wiki/index.php/LoggingBestPractices">CEDPS
	 *      "Best Practices" format</a>
	 */
	public String toString() {
		/*if (micro2 > 0) {
			addTimeStamp();
			micro2 = 0;
		}*/
		return buf.toString();
	}

	/**
	 * Add a timestamp to the message.
	 */
	@SuppressWarnings("unused")
  private void addTimeStamp() {
		// re-use or re-set whole seconds
		if (micro2 / 1000000L != micro1 / 1000000L) {
			timeStringLock.lock();
			timeString = format.format(new Date(micro2 / 1000L));
			timeStringLock.unlock();
			micro1 = micro2;
		}
		buf.replace(dateStart, dateEnd, timeString);

		// add fractional time (microseconds)
		long div, frac;
		int i;
		frac = micro2 % 1000000L;
		for (i = 0, div = 100000L; i < 6; div = div / 10, i++) {
			long n = frac / div;
			buf.setCharAt(usecStart + i, DIGIT[(int) n]);
			frac -= n * div;
		}
	}

	// ===============================================================
	// Log4J compatibility (contributed by Wolfgang Hoschek)
	// ===============================================================

	/**
	 * Static class initializer.
	 * 
	 * Make it so that log4j.jar is a compile time requirement, but not a
	 * runtime requirement
	 */
	static {
		try {
			// check if log4j is present
			Class.forName("org.apache.log4j.spi.Filter");
			// executed only if log4j is present
			Log4jFilter.init();
		} catch (ClassNotFoundException e) {
			// This warning might mess up daemon processes,
			// so it's commented out by default
			System.err.println("Warning: Cannot find log4j " + "(org.apache.log4j.spi.Filter), "
					+ "continuing..");
		}
		// set calendar of formatter: otherwise no UTC!!
		format.setCalendar(calendar);
		// init base nanosecond and millisecond time
		long ms = System.currentTimeMillis();
		nano0 = System.nanoTime();
		micro0 = micro1 = ms * 1000L;
		timeString = format.format(new Date(ms));
	}

	/**
	 * In log4j, ignore all messages not specifically directed at this appender.
	 */
	private static final class Log4jFilter extends org.apache.log4j.spi.Filter {
		public static void init() {
			Enumeration<?> loggers = org.apache.log4j.Logger.getRootLogger().getLoggerRepository()
					.getCurrentLoggers();
			while (loggers.hasMoreElements()) {
				org.apache.log4j.Logger logger = (org.apache.log4j.Logger) loggers.nextElement();
				if (logger.getAppender(APPENDER) != null) {
					logger.getAppender(APPENDER).addFilter(new Log4jFilter());
				}
			}
		}

		public int decide(org.apache.log4j.spi.LoggingEvent event) {
			if (event.getMessage() instanceof EventLogMessage) {
				return NEUTRAL; // let message pass through
			} else {
				return DENY; // ignore all non-netlogger messages
			}
		}
	}
}
